#include <initializer_list>
#include <memory>
#include <cstdlib>
#include <cstring>
#include <jni.h>
#include <cerrno>
#include <cassert>
#include <EGL/egl.h>
#include <GLES3/gl32.h>
#include <string>
#include <android/log.h>
#include "android_native_app_glue.h"
#include "EngineCore.h"
#include "ShaderMgr.h"
#include "ResourceMgr.h"
#include "Pass.h"
#include "Buffer.h"
#include "RenderItem.h"
#include "Scene.h"
#include "MobileInput.h"
#include "Texture.h"
#include "MeshModel.h"
#include "SkyBoxPass.h"
#include "BlinnPhongLightPass.h"
#include "ShadowMapPass.h"
#include "ComputePass.h"
#include "PostProcessPass.h"
#include "InstancedItemPass.h"
#include "RenderItemInstanced.h"
#include "ComputeParticles.h"


extern "C" JNIEXPORT jstring JNICALL
Java_com_tencent_rayynxu_androidgraphicsdemos_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "欢迎来到许云楠的图形学学习demo";
    return env->NewStringUTF(hello.c_str());
}

GLESEngine::GLESEngine()
{
}

void GLESEngine::ShaderSetup()
{
    std::vector<std::string> shaderList;
    shaderList.emplace_back("simple_fs.glsl");
    shaderList.emplace_back("simple_vs.glsl");
    shaderList.emplace_back("skybox_vs.glsl");
    shaderList.emplace_back("skybox_fs.glsl");
    shaderList.emplace_back("shadowmap_fs.glsl");
    shaderList.emplace_back("shadowmap_vs.glsl");
    shaderList.emplace_back("gauss_blur_cs.glsl");
    shaderList.emplace_back("gauss_blur_fs.glsl");
    shaderList.emplace_back("compute_test_cs.glsl");
    shaderList.emplace_back("instanced_fs.glsl");
    shaderList.emplace_back("instanced_vs.glsl");
    shaderList.emplace_back("skin_mesh_vs.glsl");
    GLESShaderMgr::GetInstance()->LoadShaderList(shaderList);
}

void GLESEngine::PipelineSetup() {

    std::vector<const char *> skybox_file_list = {
            "textures/right_skybox.png",
            "textures/left_skybox.png",
            "textures/top_skybox.png",
            "textures/bottom_skybox.png",
            "textures/front_skybox.png",
            "textures/back_skybox.png",
    };

    glClearColor(0.0f, 0.5f, 0.5f, 0.0f);
    SceneMgr::GetInstance()->GetCurrentScene()->ParseScene();

    auto curr_scene = SceneMgr::GetInstance()->GetCurrentScene();
    for (auto pass = curr_scene->GetPassListBegin(); pass != curr_scene->GetPassListEnd(); ++pass)
    {

        if (*pass == "shadowmap")
        {
            auto shadowmap_pass = std::make_shared<ShadowMapPass<GraphicsAPI::GLESv3>>();
            std::unordered_map<ShaderType, std::string> shadowmap_shader_dict({
                  {ShaderType::VertexShader, "shaders/shadowmap_vs.glsl"},
                  {ShaderType::FragmentShader, "shaders/shadowmap_fs.glsl"}
            });
            shadowmap_pass->InitShaders(shadowmap_shader_dict);
            passList.emplace_back(shadowmap_pass);
        }
        else if (*pass == "compute")
        {
            if (curr_scene->GetInstancedItemBegin() != curr_scene->GetInstancedItemEnd())
            {
                auto instance_item = std::dynamic_pointer_cast<RenderItemInstanced<GraphicsAPI::GLESv3>>(*curr_scene->GetInstancedItemBegin());
                auto compute_pass = std::make_shared<ComputePass<GraphicsAPI::GLESv3>>(instance_item->particleList.data(), sizeof(ParticleStructure),instance_item->particleList.size());
                std::unordered_map<ShaderType, std::string> compute_pass_shader_dict({
                     {ShaderType::ComputeShader, "shaders/compute_test_cs.glsl"},
             });
                compute_pass->InitShaders(compute_pass_shader_dict);
                passList.emplace_back(compute_pass);
            }
        }
        else if (*pass == "skybox")
        {
            auto skybox_pass = std::make_shared<SkyBoxPass<GraphicsAPI::GLESv3>>(skybox_file_list);
            std::unordered_map<ShaderType, std::string> skybox_shader_dict({
               {ShaderType::VertexShader, "shaders/skybox_vs.glsl"},
               {ShaderType::FragmentShader, "shaders/skybox_fs.glsl"}
            });
            skybox_pass->InitShaders(skybox_shader_dict);
            passList.emplace_back(skybox_pass);
        }
        else if (*pass == "blinnphong")
        {
            auto blinnphong_pass = std::make_shared<BlinnPhongLightPass<GraphicsAPI::GLESv3>>();
            std::unordered_map<ShaderType, std::string> blinnphong_shader_dict({
               {ShaderType::VertexShader, "shaders/simple_vs.glsl"},
               {ShaderType::FragmentShader, "shaders/simple_fs.glsl"}
            });
            blinnphong_pass->InitShaders(blinnphong_shader_dict);
            passList.emplace_back(blinnphong_pass);
        }
        else if (*pass == "postprocess")
        {
            auto postprocess_pass = std::make_shared<PostProcessPass<GraphicsAPI::GLESv3>>();

            // fragment shader 和 compute shader 两种实现
            std::unordered_map<ShaderType, std::string> postprocess_pass_shader_dict({
                {ShaderType::ComputeShader, "shaders/gauss_blur_cs.glsl"},
            });
            postprocess_pass->InitShaders(postprocess_pass_shader_dict);
            passList.emplace_back(postprocess_pass);
        }
        else if (*pass == "instanced")
        {
            auto instanced_pass = std::make_shared<InstancedItemPass<GraphicsAPI::GLESv3>>();

            // fragment shader 和 compute shader 两种实现
            std::unordered_map<ShaderType, std::string> instanced_pass_shader_dict({
               {ShaderType::VertexShader, "shaders/instanced_vs.glsl"},
               {ShaderType::FragmentShader, "shaders/instanced_fs.glsl"}
            });
            instanced_pass->InitShaders(instanced_pass_shader_dict);
            passList.emplace_back(instanced_pass);
        }
        else if (*pass == "skinnedmesh")
        {
            auto skinnedmesh_pass = std::make_shared<BlinnPhongLightPass<GraphicsAPI::GLESv3>>(true);
            std::unordered_map<ShaderType, std::string> skinned_shader_dict({
                   {ShaderType::VertexShader, "shaders/skin_mesh_vs.glsl"},
                   {ShaderType::FragmentShader, "shaders/simple_fs.glsl"}
           });
            skinnedmesh_pass->InitShaders(skinned_shader_dict);
            passList.emplace_back(skinnedmesh_pass);
        }
        else
        {
            LOGE("PipelineSetup unkonwn pass name %s", pass->c_str());
        }
    }
}

void GLESEngine::SceneSetup(const char * scene_name)
{
    SceneMgr::GetInstance()->LoadScene(scene_name);
}

void GLESEngine::Draw()
{
    GL_CHECK(glViewport(0, 0, width, height));
    GL_CHECK(glClear(GL_COLOR_BUFFER_BIT));
    GL_CHECK(glClear(GL_DEPTH_BUFFER_BIT));

    auto now = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = now - lastTickEngineTime;
    std::chrono::duration<double> tot_seconds = now - startEngineTime;

    if (tot_seconds.count() > 1.0) {
        SceneMgr::GetInstance()->GetCurrentScene()->Update(elapsed_seconds.count(),
                                                           tot_seconds.count() - 1.0);
    }
    lastTickEngineTime = now;

    for (auto & pass_ptr : passList)
    {
        pass_ptr.get()->Draw();
    }
}

GLESEngine * g_engine = nullptr;

/**
 * Initialize an EGL context for the current display.
 */
static int engine_init_display(GLESEngine* engine) {
    // initialize OpenGL ES and EGL

    /*
     * Here specify the attributes of the desired configuration.
     * Below, we select an EGLConfig with at least 8 bits per color
     * component compatible with on-screen windows
     */

    const EGLint attribs[] = {
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_BLUE_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_RED_SIZE, 8,
            EGL_ALPHA_SIZE, 8,
            EGL_DEPTH_SIZE, 1,
            EGL_CONFORMANT, EGL_OPENGL_ES3_BIT,
            EGL_NONE
    };
    EGLint w, h, format;
    EGLint numConfigs;
    EGLConfig config = nullptr;
    EGLSurface surface;
    EGLContext context;

    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);

    eglInitialize(display, nullptr, nullptr);

    /* Here, the application chooses the configuration it desires.
     * find the best match if possible, otherwise use the very first one
     */
    eglChooseConfig(display, attribs, nullptr,0, &numConfigs);
    std::unique_ptr<EGLConfig[]> supportedConfigs(new EGLConfig[numConfigs]);
    assert(supportedConfigs);
    eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &numConfigs);
    assert(numConfigs);
    auto i = 0;
    for (; i < numConfigs; i++)
    {
        auto& cfg = supportedConfigs[i];

        LOGI("i = %d, cfg = %d", i, static_cast<int>(reinterpret_cast<std::uintptr_t>(cfg)));

        EGLint r, g, b, d, conformant;
        if (eglGetConfigAttrib(display, cfg, EGL_RED_SIZE, &r)   &&
            eglGetConfigAttrib(display, cfg, EGL_GREEN_SIZE, &g) &&
            eglGetConfigAttrib(display, cfg, EGL_BLUE_SIZE, &b)  &&
            eglGetConfigAttrib(display, cfg, EGL_DEPTH_SIZE, &d) &&
            eglGetConfigAttrib(display, cfg, EGL_CONFORMANT, &conformant) &&
            r == 8 && g == 8 && b == 8 && d  && (conformant & EGL_OPENGL_ES3_BIT)) {
            config = supportedConfigs[i];
            break;
        }
    }
    if (i == numConfigs) {
        config = supportedConfigs[0];
    }

    LOGI("numConfigs = %d", numConfigs);

    if (config == nullptr) {
        LOGW("Unable to initialize EGLConfig");
        return -1;
    }

    /* EGL_NATIVE_VISUAL_ID is an attribute of the EGLConfig that is
     * guaranteed to be accepted by ANativeWindow_setBuffersGeometry().
     * As soon as we picked a EGLConfig, we can safely reconfigure the
     * ANativeWindow buffers to match, using EGL_NATIVE_VISUAL_ID. */
    eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
    surface = eglCreateWindowSurface(display, config, engine->app->window, nullptr);

    int attrib_list[] = {
            EGL_CONTEXT_CLIENT_VERSION, 3,
            EGL_NONE
    };

    context = eglCreateContext(display, config, nullptr, attrib_list);

    if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
        LOGW("Unable to eglMakeCurrent");
        return -1;
    }

    eglQuerySurface(display, surface, EGL_WIDTH, &w);
    eglQuerySurface(display, surface, EGL_HEIGHT, &h);

    engine->display = display;
    engine->context = context;
    engine->surface = surface;
    engine->width = w;
    engine->height = h;

    // Check openGL on the system
    auto opengl_info = {GL_VENDOR, GL_RENDERER, GL_VERSION, GL_EXTENSIONS};
    for (auto name : opengl_info) {
        auto info = glGetString(name);
        LOGI("OpenGL Info: %s", info);
    }
    // Initialize GL state.
    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

    engine->ShaderSetup();
    engine->PipelineSetup();
    return 0;
}

/**
 * Just the current frame in the display.
 */
static void engine_draw_frame(GLESEngine* engine) {
    if (engine->display == nullptr) {
        // No display.
        return;
    }

    engine->Draw();
    eglSwapBuffers(engine->display, engine->surface);
}

/**
 * Tear down the EGL context currently associated with the display.
 */
static void engine_term_display(GLESEngine* engine) {

    return;

    if (engine->display != EGL_NO_DISPLAY) {
        eglMakeCurrent(engine->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        if (engine->context != EGL_NO_CONTEXT) {
            eglDestroyContext(engine->display, engine->context);
        }
        if (engine->surface != EGL_NO_SURFACE) {
            eglDestroySurface(engine->display, engine->surface);
        }
        eglTerminate(engine->display);
    }
    engine->animating = 0;
    engine->display = EGL_NO_DISPLAY;
    engine->context = EGL_NO_CONTEXT;
    engine->surface = EGL_NO_SURFACE;
}

/**
 * Process the next input event.
 */
static int32_t engine_handle_input(struct android_app* app, AInputEvent* event) {

//    LOGI("engine_handle_input %d, rawx = %f, rawy = %f, hisx = %f, hisy = %f", AInputEvent_getType(event),
//            AMotionEvent_getRawX(event, 0), AMotionEvent_getRawY(event, 0),
//            AMotionEvent_getHistoricalRawX(event, 0, 1), AMotionEvent_getHistoricalRawY(event, 0, 1));

    MobileInputMgr::GetInstance()->OnAInputEvent(event);

    auto* engine = (GLESEngine*)app->userData;
    if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
        engine->animating = 1;
        return 1;
    }
    return 0;
}

/**
 * Process the next main command.
 */
static void engine_handle_cmd(struct android_app* app, int32_t cmd) {
    auto* engine = (GLESEngine*)app->userData;
    switch (cmd) {

        case APP_CMD_SAVE_STATE:
            // The system has asked us to save our current state.  Do so.
            break;

        case APP_CMD_INIT_WINDOW:
            // The window is being shown, get it ready.
            if (engine->app->window != nullptr) {
                engine_init_display(engine);
                engine_draw_frame(engine);
            }
            break;

        case APP_CMD_TERM_WINDOW:
            // The window is being hidden or closed, clean it up.
            engine_term_display(engine);
            break;

        case APP_CMD_GAINED_FOCUS:
            // When our app gains focus, we start monitoring the accelerometer.
            break;

        case APP_CMD_LOST_FOCUS:
            // engine->animating = 0;
            engine_draw_frame(engine);
            break;

        default:
            break;
    }
}

void android_main(struct android_app* state)
{
    g_engine = new GLESEngine();
    state->userData = g_engine;
    state->onAppCmd = engine_handle_cmd;
    state->onInputEvent = engine_handle_input;
    g_engine->app = state;

    JNIEnv *env;
    state->activity->vm->AttachCurrentThread(&env, 0);

    jobject me = state->activity->clazz;

    jclass acl = env->GetObjectClass(me); //class pointer of NativeActivity
    jmethodID giid = env->GetMethodID(acl, "getIntent", "()Landroid/content/Intent;");
    jobject intent = env->CallObjectMethod(me, giid); //Got our intent

    jclass icl = env->GetObjectClass(intent); //class pointer of Intent
    jmethodID gseid = env->GetMethodID(icl, "getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;");

    jstring jscene_str = (jstring)env->CallObjectMethod(intent, gseid, env->NewStringUTF("scene"));
    const char *scene_str = env->GetStringUTFChars(jscene_str, 0);
    g_engine->SceneSetup(scene_str);
    env->ReleaseStringUTFChars(jscene_str, scene_str);

    if (state->savedState != nullptr) {
        // We are starting with a previous saved state; restore from it.
    }

    // loop waiting for stuff to do.

    g_engine->startEngineTime = std::chrono::system_clock::now();

    while (true) {
        // Read all pending events.
        int ident;
        int events;
        struct android_poll_source* source;

        // If not animating, we will block forever waiting for events.
        // If animating, we loop until all events are read, then continue
        // to draw the next frame of animation.
        while ((ident=ALooper_pollAll(0, nullptr, &events,
                                      (void**)&source)) >= 0) {

            // Process this event.
            if (source != nullptr) {
                source->process(state, source);
            }

            // If a sensor has data, process it now.
            if (ident == LOOPER_ID_USER) {
            }

            // Check if we are exiting.
            if (state->destroyRequested != 0) {
                engine_term_display(g_engine);
                return;
            }
        }

        // if (g_engine->animating) {
            engine_draw_frame(g_engine);
        // }
    }
}